#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#include <assert.h>
#include <ctype.h>
#include "ILexer.h"
#include "Scintilla.h"
#include "SciLexer.h"
#include "WordList.h"
#include "LexAccessor.h"
#include "Accessor.h"
#include "StyleContext.h"
#include "CharacterSet.h"
#include "LexerModule.h"

#ifdef SCI_NAMESPACE
using namespace Scintilla;
#endif

static inline bool IsAWordChar( int ch ) {
  return ( ch < 0x80 ) && ( isalnum( ch ) || ch == '_' );
}

static inline bool IsAWordStart( int ch ) {
  return ( ch < 0x80 ) && ( isalpha( ch ) || ch == '_' );
}

static inline bool IsANumberChar( int ch ) {
  return ( ch < 0x80 ) && ( isdigit( ch ) || toupper( ch ) == 'E' ||
                            ch == '.' || ch == '-' || ch == '+' );
}

static void CheckForKeyword( StyleContext& sc, WordList* keywordlists[], int activeState ) {
  int length = sc.LengthCurrent() + 1; // +1 for the next char
  char* s = new char[length];
  sc.GetCurrentLowered( s, length );
  if( keywordlists[0]->InList( s ) ) {
    sc.ChangeState( SCE_MYSQL_MAJORKEYWORD | activeState );
  } else if( keywordlists[1]->InList( s ) ) {
    sc.ChangeState( SCE_MYSQL_KEYWORD | activeState );
  } else if( keywordlists[2]->InList( s ) ) {
    sc.ChangeState( SCE_MYSQL_DATABASEOBJECT | activeState );
  } else if( keywordlists[3]->InList( s ) ) {
    sc.ChangeState( SCE_MYSQL_FUNCTION | activeState );
  } else if( keywordlists[5]->InList( s ) ) {
    sc.ChangeState( SCE_MYSQL_PROCEDUREKEYWORD | activeState );
  } else if( keywordlists[6]->InList( s ) ) {
    sc.ChangeState( SCE_MYSQL_USER1 | activeState );
  } else if( keywordlists[7]->InList( s ) ) {
    sc.ChangeState( SCE_MYSQL_USER2 | activeState );
  } else if( keywordlists[8]->InList( s ) ) {
    sc.ChangeState( SCE_MYSQL_USER3 | activeState );
  }
  delete [] s;
}

#define HIDDENCOMMAND_STATE 0x40 // Offset for states within a hidden command.
#define MASKACTIVE(style) (style & ~HIDDENCOMMAND_STATE)

static void SetDefaultState( StyleContext& sc, int activeState ) {
  if( activeState == 0 ) {
    sc.SetState( SCE_MYSQL_DEFAULT );
  } else {
    sc.SetState( SCE_MYSQL_HIDDENCOMMAND );
  }
}

static void ForwardDefaultState( StyleContext& sc, int activeState ) {
  if( activeState == 0 ) {
    sc.ForwardSetState( SCE_MYSQL_DEFAULT );
  } else {
    sc.ForwardSetState( SCE_MYSQL_HIDDENCOMMAND );
  }
}

static void ColouriseMySQLDoc( unsigned int startPos, int length, int initStyle, WordList *keywordlists[],
                               Accessor &styler ) {
  StyleContext sc( startPos, length, initStyle, styler, 127 );
  int activeState = ( initStyle == SCE_MYSQL_HIDDENCOMMAND ) ? HIDDENCOMMAND_STATE : initStyle & HIDDENCOMMAND_STATE;
  for( ; sc.More(); sc.Forward() ) {
    switch( MASKACTIVE( sc.state ) ) {
      case SCE_MYSQL_OPERATOR:
        SetDefaultState( sc, activeState );
        break;
      case SCE_MYSQL_NUMBER:
        if( !IsANumberChar( sc.ch ) ) {
          SetDefaultState( sc, activeState );
        }
        break;
      case SCE_MYSQL_IDENTIFIER:
        if( !IsAWordChar( sc.ch ) ) {
          CheckForKeyword( sc, keywordlists, activeState );
          if( MASKACTIVE( sc.state ) == SCE_MYSQL_FUNCTION && sc.ch != '(' ) {
            if( activeState > 0 ) {
              sc.ChangeState( SCE_MYSQL_HIDDENCOMMAND );
            } else
            { sc.ChangeState( SCE_MYSQL_DEFAULT ); }
          }
          SetDefaultState( sc, activeState );
        }
        break;
      case SCE_MYSQL_VARIABLE:
        if( !IsAWordChar( sc.ch ) ) {
          SetDefaultState( sc, activeState );
        }
        break;
      case SCE_MYSQL_SYSTEMVARIABLE:
        if( !IsAWordChar( sc.ch ) ) {
          int length = sc.LengthCurrent() + 1;
          char* s = new char[length];
          sc.GetCurrentLowered( s, length );
          if( keywordlists[4]->InList( &s[2] ) ) {
            sc.ChangeState( SCE_MYSQL_KNOWNSYSTEMVARIABLE | activeState );
          }
          delete [] s;
          SetDefaultState( sc, activeState );
        }
        break;
      case SCE_MYSQL_QUOTEDIDENTIFIER:
        if( sc.ch == '`' ) {
          if( sc.chNext == '`' ) {
            sc.Forward();
          }    // Ignore it
          else
          { ForwardDefaultState( sc, activeState ); }
        }
        break;
      case SCE_MYSQL_COMMENT:
        if( sc.Match( '*', '/' ) ) {
          sc.Forward();
          ForwardDefaultState( sc, activeState );
        }
        break;
      case SCE_MYSQL_COMMENTLINE:
        if( sc.atLineStart ) {
          SetDefaultState( sc, activeState );
        }
        break;
      case SCE_MYSQL_SQSTRING:
        if( sc.ch == '\\' ) {
          sc.Forward();
        }   // Escape sequence
        else if( sc.ch == '\'' ) {
          if( sc.chNext == '\'' ) {
            sc.Forward();
          } else
          { ForwardDefaultState( sc, activeState ); }
        }
        break;
      case SCE_MYSQL_DQSTRING:
        if( sc.ch == '\\' ) {
          sc.Forward();
        }   // Escape sequence
        else if( sc.ch == '\"' ) {
          if( sc.chNext == '\"' ) {
            sc.Forward();
          } else
          { ForwardDefaultState( sc, activeState ); }
        }
        break;
      case SCE_MYSQL_PLACEHOLDER:
        if( sc.Match( '}', '>' ) ) {
          sc.Forward();
          ForwardDefaultState( sc, activeState );
        }
        break;
    }
    if( sc.state == SCE_MYSQL_HIDDENCOMMAND && sc.Match( '*', '/' ) ) {
      activeState = 0;
      sc.Forward();
      ForwardDefaultState( sc, activeState );
    }
    if( sc.state == SCE_MYSQL_DEFAULT || sc.state == SCE_MYSQL_HIDDENCOMMAND ) {
      switch( sc.ch ) {
        case '@':
          if( sc.chNext == '@' ) {
            sc.SetState( SCE_MYSQL_SYSTEMVARIABLE | activeState );
            sc.Forward( 2 ); // Skip past @@.
          } else if( IsAWordStart( sc.ch ) ) {
            sc.SetState( SCE_MYSQL_VARIABLE | activeState );
            sc.Forward(); // Skip past @.
          } else
          { sc.SetState( SCE_MYSQL_OPERATOR | activeState ); }
          break;
        case '`':
          sc.SetState( SCE_MYSQL_QUOTEDIDENTIFIER | activeState );
          break;
        case '#':
          sc.SetState( SCE_MYSQL_COMMENTLINE | activeState );
          break;
        case '\'':
          sc.SetState( SCE_MYSQL_SQSTRING | activeState );
          break;
        case '\"':
          sc.SetState( SCE_MYSQL_DQSTRING | activeState );
          break;
        default:
          if( IsADigit( sc.ch ) || ( sc.ch == '.' && IsADigit( sc.chNext ) ) ) {
            sc.SetState( SCE_MYSQL_NUMBER | activeState );
          } else if( IsAWordStart( sc.ch ) ) {
            sc.SetState( SCE_MYSQL_IDENTIFIER | activeState );
          } else if( sc.Match( '/', '*' ) ) {
            sc.SetState( SCE_MYSQL_COMMENT | activeState );
            sc.Forward( 2 );
            if( sc.ch == '!' ) {
              activeState = HIDDENCOMMAND_STATE;
              sc.ChangeState( SCE_MYSQL_HIDDENCOMMAND );
            }
          } else if( sc.Match( '<', '{' ) ) {
            sc.SetState( SCE_MYSQL_PLACEHOLDER | activeState );
          } else if( sc.Match( "--" ) ) {
            sc.SetState( SCE_MYSQL_COMMENTLINE | activeState );
            sc.Forward( 2 );
            if( sc.ch != ' ' && sc.ch != '\n' && sc.ch != '\r' ) {
              sc.ChangeState( SCE_MYSQL_OPERATOR | activeState );
            }
          } else if( isoperator( static_cast<char>( sc.ch ) ) ) {
            sc.SetState( SCE_MYSQL_OPERATOR | activeState );
          }
      }
    }
  }
  if( sc.state == SCE_MYSQL_IDENTIFIER ) {
    CheckForKeyword( sc, keywordlists, activeState );
    if( sc.state == SCE_MYSQL_FUNCTION && sc.ch != '(' ) {
      SetDefaultState( sc, activeState );
    }
  }
  sc.Complete();
}

static bool IsStreamCommentStyle( int style ) {
  return MASKACTIVE( style ) == SCE_MYSQL_COMMENT;
}

bool MatchIgnoreCase( Accessor &styler, int currentPos, const char *s ) {
  for( int n = 0; *s; n++ ) {
    if( *s != tolower( styler.SafeGetCharAt( currentPos + n ) ) ) {
      return false;
    }
    s++;
  }
  return true;
}

static void FoldMySQLDoc( unsigned int startPos, int length, int initStyle, WordList *[], Accessor &styler ) {
  bool foldComment = styler.GetPropertyInt( "fold.comment" ) != 0;
  bool foldCompact = styler.GetPropertyInt( "fold.compact", 1 ) != 0;
  bool foldOnlyBegin = styler.GetPropertyInt( "fold.sql.only.begin", 0 ) != 0;
  int visibleChars = 0;
  int lineCurrent = styler.GetLine( startPos );
  int levelCurrent = SC_FOLDLEVELBASE;
  if( lineCurrent > 0 ) {
    levelCurrent = styler.LevelAt( lineCurrent - 1 ) >> 16;
  }
  int levelNext = levelCurrent;
  int styleNext = styler.StyleAt( startPos );
  int style = initStyle;
  int activeState = ( style == SCE_MYSQL_HIDDENCOMMAND ) ? HIDDENCOMMAND_STATE : style & HIDDENCOMMAND_STATE;
  bool endPending = false;
  bool whenPending = false;
  bool elseIfPending = false;
  char nextChar = styler.SafeGetCharAt( startPos );
  for( unsigned int i = startPos; length > 0; i++, length-- ) {
    int stylePrev = style;
    int lastActiveState = activeState;
    style = styleNext;
    styleNext = styler.StyleAt( i + 1 );
    activeState = ( style == SCE_MYSQL_HIDDENCOMMAND ) ? HIDDENCOMMAND_STATE : style & HIDDENCOMMAND_STATE;
    char currentChar = nextChar;
    nextChar = styler.SafeGetCharAt( i + 1 );
    bool atEOL = ( currentChar == '\r' && nextChar != '\n' ) || ( currentChar == '\n' );
    switch( MASKACTIVE( style ) ) {
      case SCE_MYSQL_COMMENT:
        if( foldComment ) {
          if( IsStreamCommentStyle( style ) && !IsStreamCommentStyle( stylePrev ) ) {
            levelNext++;
          }
        }
        break;
      case SCE_MYSQL_COMMENTLINE:
        if( foldComment ) {
          if( styler.Match( i, "--" ) ) {
            char chNext2 = styler.SafeGetCharAt( i + 2 );
            char chNext3 = styler.SafeGetCharAt( i + 3 );
            if( chNext2 == '{' || chNext3 == '{' ) {
              levelNext++;
            } else if( chNext2 == '}' || chNext3 == '}' ) {
              levelNext--;
            }
          }
        }
        break;
      case SCE_MYSQL_HIDDENCOMMAND:
        if( activeState != lastActiveState ) {
          levelNext++;
        }
        break;
      case SCE_MYSQL_OPERATOR:
        if( endPending ) {
          endPending = false;
          levelNext--;
          if( levelNext < SC_FOLDLEVELBASE ) {
            levelNext = SC_FOLDLEVELBASE;
          }
        }
        if( currentChar == '(' ) {
          levelNext++;
        } else if( currentChar == ')' ) {
          levelNext--;
          if( levelNext < SC_FOLDLEVELBASE ) {
            levelNext = SC_FOLDLEVELBASE;
          }
        }
        break;
      case SCE_MYSQL_MAJORKEYWORD:
      case SCE_MYSQL_KEYWORD:
      case SCE_MYSQL_FUNCTION:
      case SCE_MYSQL_PROCEDUREKEYWORD:
        if( style != stylePrev ) {
          bool endFound = MatchIgnoreCase( styler, i, "end" );
          if( endPending ) {
            levelNext--;
            if( levelNext < SC_FOLDLEVELBASE ) {
              levelNext = SC_FOLDLEVELBASE;
            }
          } else if( !endFound ) {
            if( MatchIgnoreCase( styler, i, "begin" ) ) {
              levelNext++;
            } else {
              if( !foldOnlyBegin ) {
                bool whileFound = MatchIgnoreCase( styler, i, "while" );
                bool loopFound = MatchIgnoreCase( styler, i, "loop" );
                bool repeatFound = MatchIgnoreCase( styler, i, "repeat" );
                bool caseFound = MatchIgnoreCase( styler, i, "case" );
                if( whileFound || loopFound || repeatFound || caseFound )
                { levelNext++; }
                else {
                  if( MatchIgnoreCase( styler, i, "then" ) ) {
                    if( !elseIfPending && !whenPending )
                    { levelNext++; }
                    else {
                      elseIfPending = false;
                      whenPending = false;
                    }
                  } else {
                    if( MatchIgnoreCase( styler, i, "elseif" ) )
                    { elseIfPending = true; }
                    if( MatchIgnoreCase( styler, i, "when" ) )
                    { whenPending = true; }
                  }
                }
              }
            }
          }
          endPending = endFound;
        }
        break;
      default:
        if( !isspacechar( currentChar ) && endPending ) {
          levelNext--;
          if( levelNext < SC_FOLDLEVELBASE ) {
            levelNext = SC_FOLDLEVELBASE;
          }
        }
        break;
    }
    if( IsStreamCommentStyle( stylePrev ) && !IsStreamCommentStyle( style ) ) {
      levelNext--;
      if( levelNext < SC_FOLDLEVELBASE ) {
        levelNext = SC_FOLDLEVELBASE;
      }
    }
    if( activeState == 0 && lastActiveState != 0 ) {
      levelNext--;
      if( levelNext < SC_FOLDLEVELBASE ) {
        levelNext = SC_FOLDLEVELBASE;
      }
    }
    if( atEOL ) {
      int levelUse = levelCurrent;
      int lev = levelUse | levelNext << 16;
      if( visibleChars == 0 && foldCompact ) {
        lev |= SC_FOLDLEVELWHITEFLAG;
      }
      if( levelUse < levelNext ) {
        lev |= SC_FOLDLEVELHEADERFLAG;
      }
      if( lev != styler.LevelAt( lineCurrent ) ) {
        styler.SetLevel( lineCurrent, lev );
      }
      lineCurrent++;
      levelCurrent = levelNext;
      visibleChars = 0;
    }
    if( !isspacechar( currentChar ) ) {
      visibleChars++;
    }
  }
}

static const char * const mysqlWordListDesc[] = {
  "Major Keywords",
  "Keywords",
  "Database Objects",
  "Functions",
  "System Variables",
  "Procedure keywords",
  "User Keywords 1",
  "User Keywords 2",
  "User Keywords 3",
  0
};

LexerModule lmMySQL( SCLEX_MYSQL, ColouriseMySQLDoc, "mysql", FoldMySQLDoc, mysqlWordListDesc );
